/*
 * Copyright 2025 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.jpa.repository.aot;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.TemporalType;
import jakarta.persistence.metamodel.EmbeddableType;
import jakarta.persistence.metamodel.EntityType;
import jakarta.persistence.metamodel.ManagedType;
import jakarta.persistence.metamodel.Metamodel;
import jakarta.persistence.spi.PersistenceUnitInfo;

import java.net.URL;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;

import org.hibernate.cfg.JdbcSettings;
import org.hibernate.cfg.PersistenceSettings;
import org.hibernate.cfg.QuerySettings;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.pagination.LimitHandler;
import org.hibernate.dialect.pagination.OffsetFetchLimitHandler;
import org.hibernate.dialect.sequence.ANSISequenceSupport;
import org.hibernate.dialect.sequence.SequenceSupport;
import org.hibernate.engine.jdbc.connections.internal.UserSuppliedConnectionProviderImpl;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl;
import org.hibernate.jpa.boot.internal.PersistenceUnitInfoDescriptor;
import org.hibernate.query.common.TemporalUnit;
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.util.Lazy;
import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes;
import org.springframework.orm.jpa.persistenceunit.SpringPersistenceUnitInfo;
import org.springframework.util.CollectionUtils;

/**
 * AOT metamodel implementation that uses Hibernate to build the metamodel.
 *
 * @author Christoph Strobl
 * @author Mark Paluch
 * @author Oliver Drotbohm
 * @since 4.0
 */
class AotMetamodel implements Metamodel {

	private static final Logger log = LoggerFactory.getLogger(AotMetamodel.class);
	
	/**
	 * Collection of know properties causing problems during AOT if set differntly
	 */
	private static final Map<String, Object> FAILSAFE_AOT_PROPERTIES = Map.of( //
			JdbcSettings.ALLOW_METADATA_ON_BOOT, false, //
			JdbcSettings.CONNECTION_PROVIDER, NoOpConnectionProvider.INSTANCE, //
			QuerySettings.QUERY_STARTUP_CHECKING, false, //
			PersistenceSettings.JPA_CALLBACKS_ENABLED, false //
	);
	private final Lazy<EntityManagerFactory> entityManagerFactory;
	private final Lazy<EntityManager> entityManager = Lazy.of(() -> getEntityManagerFactory().createEntityManager());

	public AotMetamodel(PersistenceManagedTypes managedTypes, Map<String, Object> jpaProperties) {
		this(managedTypes.getManagedClassNames(), managedTypes.getPersistenceUnitRootUrl(), jpaProperties);
	}

	public AotMetamodel(Collection<String> managedTypes, @Nullable URL persistenceUnitRootUrl,
			Map<String, Object> jpaProperties) {

		SpringPersistenceUnitInfo persistenceUnitInfo = new SpringPersistenceUnitInfo(
				managedTypes.getClass().getClassLoader());
		persistenceUnitInfo.setPersistenceUnitName("AotMetamodel");
		persistenceUnitInfo.setPersistenceUnitRootUrl(persistenceUnitRootUrl);

		this.entityManagerFactory = init(() -> {

			managedTypes.forEach(persistenceUnitInfo::addManagedClassName);

			persistenceUnitInfo.setPersistenceProviderClassName(HibernatePersistenceProvider.class.getName());
			return new PersistenceUnitInfoDescriptor(persistenceUnitInfo.asStandardPersistenceUnitInfo());
		}, jpaProperties);
	}

	public AotMetamodel(PersistenceUnitInfo unitInfo, Map<String, Object> jpaProperties) {
		this.entityManagerFactory = init(() -> new PersistenceUnitInfoDescriptor(unitInfo), jpaProperties);
	}

	static Lazy<EntityManagerFactory> init(Supplier<PersistenceUnitInfoDescriptor> unitInfo,
			Map<String, Object> jpaProperties) {
		return Lazy.of(() -> new EntityManagerFactoryBuilderImpl(unitInfo.get(), initProperties(jpaProperties)).build());
	}

	static Map<String, Object> initProperties(Map<String, Object> jpaProperties) {

		Map<String, Object> properties = CollectionUtils
				.newLinkedHashMap(jpaProperties.size() + FAILSAFE_AOT_PROPERTIES.size() + 1);

		// we allow explicit Dialect Overrides, but put in a default one to avoid potential db access
		properties.put(JdbcSettings.DIALECT, SpringDataJpaAotDialect.INSTANCE);

		// apply user defined properties
		properties.putAll(jpaProperties);

		// override properties known to cause trouble
		applyPropertyOverrides(properties);

		return properties;
	}

	private static void applyPropertyOverrides(Map<String, Object> properties) {

		for (Map.Entry<String, Object> entry : FAILSAFE_AOT_PROPERTIES.entrySet()) {

			if (log.isDebugEnabled() && properties.containsKey(entry.getKey())) {
				log.debug("Overriding property [%s] with value [%s] for AOT Repository processing.".formatted(entry.getKey(),
						entry.getValue()));
			}

			properties.put(entry.getKey(), entry.getValue());
		}
	}

	private Metamodel getMetamodel() {
		return getEntityManagerFactory().getMetamodel();
	}

	public <X> EntityType<X> entity(Class<X> cls) {
		return getMetamodel().entity(cls);
	}

	@Override
	public EntityType<?> entity(String s) {
		return getMetamodel().entity(s);
	}

	public <X> ManagedType<X> managedType(Class<X> cls) {
		return getMetamodel().managedType(cls);
	}

	public <X> EmbeddableType<X> embeddable(Class<X> cls) {
		return getMetamodel().embeddable(cls);
	}

	public Set<ManagedType<?>> getManagedTypes() {
		return getMetamodel().getManagedTypes();
	}

	public Set<EntityType<?>> getEntities() {
		return getMetamodel().getEntities();
	}

	public Set<EmbeddableType<?>> getEmbeddables() {
		return getMetamodel().getEmbeddables();
	}

	public EntityManager entityManager() {
		return entityManager.get();
	}

	public EntityManagerFactory getEntityManagerFactory() {
		return entityManagerFactory.get();
	}

	/**
	 * A {@link Dialect} to satisfy the bootstrap requirements of {@link JdbcSettings#DIALECT} during the AOT Phase. Printed
	 * to log files (info level) when the {@link org.hibernate.engine.jdbc.env.spi.JdbcEnvironment} is created.
	 */
	@NullUnmarked
	@SuppressWarnings("deprecation")
	static class SpringDataJpaAotDialect extends Dialect {

		static SpringDataJpaAotDialect INSTANCE = new SpringDataJpaAotDialect();

		public boolean isCurrentTimestampSelectStringCallable() {
			return false;
		}

		public String getCurrentTimestampSelectString() {
			return "call current_timestamp()";
		}

		@Override
		public LimitHandler getLimitHandler() {
			return OffsetFetchLimitHandler.INSTANCE;
		}

		@Override
		public SequenceSupport getSequenceSupport() {
			return ANSISequenceSupport.INSTANCE;
		}

		@Override
		public SqlAstTranslatorFactory getSqlAstTranslatorFactory() {
			// javadoc implies null would trigger default which is not the case
			return new StandardSqlAstTranslatorFactory();
		}

		@Override
		@SuppressWarnings("deprecation")
		public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) {
			if (unit == null) {
				return "(?3-?2)";
			}
			return "datediff(?1,?2,?3)";
		}

	}

	static class NoOpConnectionProvider extends UserSuppliedConnectionProviderImpl {

		static final NoOpConnectionProvider INSTANCE = new NoOpConnectionProvider();

		@Override
		public String toString() {
			return "NoOpConnectionProvider";
		}
	}

}
